/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.lvyh.lightframe.tracer.core.zipkin;

import lombok.Data;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.Map;

/**
 * The data formats supported by Zipkin are compatible with the open trace standard
 */
@Data
public class ZipkinTraceSpan {
    /**
     * TraceId
     */
    private String traceId;

    /**
     * ParentSpanId
     */
    private String parentId;

    /**
     * SpanId
     */
    private String id;

    /**
     * Operation name
     */
    private String name;

    /**
     * Start time
     */
    private long timestamp;

    /**
     * End time=timestamp+duration
     */
    private long duration;

    /**
     * label
     */
    private Map<String, String> tags;

    /**
     * Span type, such as server or client
     */
    private String kind;

    /**
     * Describe local service information, such as service name, IP, etc., to facilitate link retrieval based on service name
     */
    private Endpoint localEndpoint;

    /**
     * Describe the service information of the remote service when calling RPC
     */
    private Endpoint remoteEndpoint;

    private static final char[] HEX_DIGITS = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    private static final int IPV6_PART_COUNT = 8;

    private static final ThreadLocal<char[]> IPV6_TO_STRING = new ThreadLocal<char[]>() {
        protected char[] initialValue() {
            return new char[39];
        }
    };

    public static enum Kind {
        CLIENT,
        SERVER,
        PRODUCER,
        CONSUMER
    }

    public static class Endpoint {
        String ip;
        String serviceName, ipv4, ipv6;
        byte[] ipv4Bytes, ipv6Bytes;
        int port; // zero means null

        public Endpoint(String serviceName) {
            this.serviceName = serviceName;
        }

        public Endpoint(String serviceName, InetAddress addr) {
            this.serviceName = serviceName;
            parseIp(addr);
        }

        public final boolean parseIp(InetAddress addr) {
            if (addr == null) {
                return false;
            } else {
                if (addr instanceof Inet4Address) {
                    this.ipv4 = addr.getHostAddress();
                    this.ipv4Bytes = addr.getAddress();
                } else {
                    if (!(addr instanceof Inet6Address)) {
                        return false;
                    }

                    byte[] addressBytes = addr.getAddress();
                    if (!this.parseEmbeddedIPv4(addressBytes)) {
                        this.ipv6 = Endpoint.writeIpV6(addressBytes);
                        this.ipv6Bytes = addressBytes;
                    }
                }

                return true;
            }
        }

        boolean parseEmbeddedIPv4(byte[] ipv6) {
            int flag;
            for (flag = 0; flag < 10; ++flag) {
                if (ipv6[flag] != 0) {
                    return false;
                }
            }

            flag = (ipv6[10] & 255) << 8 | ipv6[11] & 255;
            if (flag != 0 && flag != -1) {
                return false;
            } else {
                byte o1 = ipv6[12];
                byte o2 = ipv6[13];
                byte o3 = ipv6[14];
                byte o4 = ipv6[15];
                if (flag == 0 && o1 == 0 && o2 == 0 && o3 == 0 && o4 == 1) {
                    return false;
                } else {
                    this.ipv4 = String.valueOf(o1 & 255) + '.' + (o2 & 255) + '.' + (o3 & 255) + '.' + (o4 & 255);
                    this.ipv4Bytes = new byte[]{o1, o2, o3, o4};
                    return true;
                }
            }
        }

        static String writeIpV6(byte[] ipv6) {
            int pos = 0;
            char[] buf = (char[]) IPV6_TO_STRING.get();
            int zeroCompressionIndex = -1;
            int zeroCompressionLength = -1;
            int zeroIndex = -1;
            boolean allZeros = true;

            int i;
            for (i = 0; i < ipv6.length; i += 2) {
                if (ipv6[i] == 0 && ipv6[i + 1] == 0) {
                    if (zeroIndex < 0) {
                        zeroIndex = i;
                    }
                } else {
                    allZeros = false;
                    if (zeroIndex >= 0) {
                        int zeroLength = i - zeroIndex;
                        if (zeroLength > zeroCompressionLength) {
                            zeroCompressionIndex = zeroIndex;
                            zeroCompressionLength = zeroLength;
                        }

                        zeroIndex = -1;
                    }
                }
            }

            if (allZeros) {
                return "::";
            } else {
                if (zeroCompressionIndex == -1 && zeroIndex != -1) {
                    zeroCompressionIndex = zeroIndex;
                    zeroCompressionLength = 16 - zeroIndex;
                }

                i = 0;

                while (true) {
                    while (i < ipv6.length) {
                        if (i == zeroCompressionIndex) {
                            buf[pos++] = 58;
                            i += zeroCompressionLength;
                            if (i == ipv6.length) {
                                buf[pos++] = 58;
                            }
                        } else {
                            if (i != 0) {
                                buf[pos++] = 58;
                            }

                            byte high = ipv6[i++];
                            byte low = ipv6[i++];
                            char val = HEX_DIGITS[high >> 4 & 15];
                            boolean leadingZero;
                            if (!(leadingZero = val == 48)) {
                                buf[pos++] = val;
                            }

                            val = HEX_DIGITS[high & 15];
                            if (!(leadingZero = leadingZero && val == 48)) {
                                buf[pos++] = val;
                            }

                            val = HEX_DIGITS[low >> 4 & 15];
                            if (!leadingZero || val != 48) {
                                buf[pos++] = val;
                            }

                            buf[pos++] = HEX_DIGITS[low & 15];
                        }
                    }

                    return new String(buf, 0, pos);
                }
            }
        }
    }
}
